Emberveil source

game/2006Scape Client/src/main/java/EmberveilWorldMap.java

import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.*;

/** Offline reference atlas. All window state belongs to the AWT event thread. */
public final class EmberveilWorldMap {
    private static JDialog window;
    // Published atomically by the game loop; Swing never reads mutable game state.
    private static volatile Position position;

    static final class Position {
        final int x, y, floor;
        Position(int x, int y, int floor) { this.x=x; this.y=y; this.floor=floor; }
    }

    static void updatePosition(boolean loggedIn, int x, int y, int floor) {
        position = loggedIn ? new Position(x,y,floor) : null;
    }

    // This raster has two pixels per tile, west edge 2112 and north edge 4032.
    // Half-tile offsets place the marker at the centre of the tile.
    static Point mapPoint(Position p, BufferedImage image) {
        if(p==null || p.floor<0 || p.floor>3) return null;
        int x=(p.x-2112)*2+1, y=(4032-p.y)*2-1;
        if(x<0 || y<0 || x>=image.getWidth() || y>=image.getHeight()) return null;
        if((image.getRGB(x,y)&0xffffff)==0) return null;
        return new Point(x,y);
    }

    public static void open(Frame owner) {
        EventQueue.invokeLater(() -> {
            if (window != null && window.isDisplayable()) {
                window.setVisible(true);
                window.toFront();
                return;
            }
            try {
                MapCanvas map = new MapCanvas(loadImage());
                JDialog dialog = new JDialog(owner, "Emberveil | World Map", false);
                dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
                JPanel controls = new JPanel(new FlowLayout(FlowLayout.LEFT));
                controls.setBackground(new Color(48, 40, 30));
                for (double scale : new double[] {0.25, 0.5, 0.75, 1, 1.5, 2}) {
                    JButton button = new JButton((int) (scale * 100) + "%");
                    button.addActionListener(e -> map.zoom(scale, map.getWidth()/2, map.getHeight()/2));
                    controls.add(button);
                }
                JButton fit = new JButton("Overview");
                fit.addActionListener(e -> map.fit());
                controls.add(fit);
                JButton find = new JButton("Find me");
                find.addActionListener(e -> map.findMe());
                controls.add(find);
                JLabel location = new JLabel();
                location.setForeground(new Color(255, 220, 95));
                javax.swing.Timer tracking = new javax.swing.Timer(200, e -> {
                    Position p=position;
                    boolean available=mapPoint(p,map.image)!=null;
                    find.setEnabled(available);
                    location.setText(p==null ? "  Log in to show your position" : available
                        ? "  Yellow dot: you are here ("+p.x+", "+p.y+")"+(p.floor>0 ? " - upper floor, surface position" : "")
                        : "  Your location is underground or outside this surface map");
                    map.repaint();
                });
                find.setEnabled(mapPoint(position,map.image)!=null);
                location.setText("  Yellow dot: you are here | Find me centres on your position");
                dialog.addWindowListener(new WindowAdapter() {
                    public void windowClosed(WindowEvent e) { tracking.stop(); }
                });
                tracking.start();
                JButton close = new JButton("Close");
                close.addActionListener(e -> dialog.dispose());
                controls.add(close);
                JLabel help = new JLabel("  Drag to explore | Mouse wheel to zoom | Esc to close");
                help.setForeground(new Color(235, 218, 175));
                JPanel header = new JPanel(new BorderLayout());
                header.setBackground(controls.getBackground());
                header.add(controls, BorderLayout.CENTER);
                JPanel hints = new JPanel(new GridLayout(2,1));
                hints.setBackground(controls.getBackground());
                hints.add(help);
                hints.add(location);
                header.add(hints, BorderLayout.SOUTH);
                JLabel note = new JLabel("<html>July 2005 surface reference - later areas and dungeons are not shown.<br>Map artwork: Jagex / RuneScape Wiki.</html>");
                note.setBorder(BorderFactory.createEmptyBorder(6, 4, 6, 4));
                dialog.add(header, BorderLayout.NORTH);
                dialog.add(map, BorderLayout.CENTER);
                dialog.add(note, BorderLayout.SOUTH);
                dialog.getRootPane().registerKeyboardAction(e -> dialog.dispose(),
                    KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW);
                Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
                dialog.setSize(Math.min(1100, screen.width - 60), Math.min(800, screen.height - 80));
                dialog.setMinimumSize(new Dimension(Math.min(720, screen.width-60), Math.min(480, screen.height-80)));
                dialog.setLocationRelativeTo(owner);
                window = dialog;
                dialog.setVisible(true);
                map.fit();
            } catch (IOException | RuntimeException ex) {
                JOptionPane.showMessageDialog(owner, "The bundled world map could not be loaded.\n" + ex.getMessage(),
                    "Emberveil World Map", JOptionPane.ERROR_MESSAGE);
            }
        });
    }

    static BufferedImage loadImage() throws IOException {
        java.net.URL resource = EmberveilWorldMap.class.getResource("/emberveil-worldmap.png");
        if (resource == null) throw new IOException("Missing emberveil-worldmap.png");
        BufferedImage result = ImageIO.read(resource);
        if (result == null) throw new IOException("Unreadable map image");
        return result;
    }

    static final class MapCanvas extends JPanel {
        final BufferedImage image;
        double scale = 0.25, offsetX, offsetY;
        private Point drag;

        MapCanvas(BufferedImage image) {
            this.image = image;
            setBackground(Color.BLACK);
            setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
            MouseAdapter mouse = new MouseAdapter() {
                public void mousePressed(MouseEvent e) {
                    if (SwingUtilities.isLeftMouseButton(e)) drag = e.getPoint();
                }
                public void mouseReleased(MouseEvent e) { drag = null; }
                public void mouseDragged(MouseEvent e) {
                    if (drag == null) return;
                    offsetX += e.getX() - drag.x;
                    offsetY += e.getY() - drag.y;
                    drag = e.getPoint();
                    constrain();
                    repaint();
                }
                public void mouseWheelMoved(MouseWheelEvent e) {
                    zoom(scale * Math.pow(1.15, -e.getWheelRotation()), e.getX(), e.getY());
                }
            };
            addMouseListener(mouse);
            addMouseMotionListener(mouse);
            addMouseWheelListener(mouse);
            addComponentListener(new ComponentAdapter() {
                public void componentResized(ComponentEvent e) { constrain(); repaint(); }
            });
        }

        void fit() {
            scale = Math.min((double)getWidth()/image.getWidth(), (double)getHeight()/image.getHeight());
            scale = Math.max(0.1, Math.min(2, scale));
            offsetX = (getWidth() - image.getWidth()*scale)/2;
            offsetY = (getHeight() - image.getHeight()*scale)/2;
            repaint();
        }

        void findMe() {
            Point point=mapPoint(position,image);
            if(point==null) return;
            scale=Math.max(scale,0.75);
            offsetX=getWidth()/2.0-point.x*scale;
            offsetY=getHeight()/2.0-point.y*scale;
            constrain();
            repaint();
        }

        void zoom(double value, int x, int y) {
            double next = Math.max(0.1, Math.min(2, value));
            offsetX = x - (x - offsetX) * next/scale;
            offsetY = y - (y - offsetY) * next/scale;
            scale = next;
            constrain();
            repaint();
        }

        private void constrain() {
            double width = image.getWidth()*scale, height = image.getHeight()*scale;
            offsetX = width <= getWidth() ? (getWidth()-width)/2 : Math.max(getWidth()-width, Math.min(0, offsetX));
            offsetY = height <= getHeight() ? (getHeight()-height)/2 : Math.max(getHeight()-height, Math.min(0, offsetY));
        }

        protected void paintComponent(Graphics graphics) {
            super.paintComponent(graphics);
            Graphics2D g = (Graphics2D)graphics.create();
            g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
            g.translate(offsetX, offsetY);
            g.scale(scale, scale);
            g.drawImage(image, 0, 0, null);
            g.dispose();
            Point point=mapPoint(position,image);
            if(point!=null) {
                int x=(int)Math.round(offsetX+point.x*scale), y=(int)Math.round(offsetY+point.y*scale);
                Graphics2D marker=(Graphics2D)graphics.create();
                marker.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
                marker.setColor(Color.BLACK); marker.fillOval(x-8,y-8,16,16);
                marker.setColor(Color.WHITE); marker.fillOval(x-6,y-6,12,12);
                marker.setColor(new Color(255,210,30)); marker.fillOval(x-4,y-4,8,8);
                marker.dispose();
            }
        }
    }
}